﻿/*
COPYRIGHT NOTICE

Copyright (c) 2011, S James S Stapleton
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
	  this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
	  this list of conditions and the following disclaimer in the documentation
	  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Reflection;
using ConfigFileManager;

namespace LocalL
{
    /// <summary>
    /// This is the localization engine. It can be interacted with in two ways.you can create new
    /// engines with "Localizer MyLoc = new Localizer(...)", and then access them using 
    /// MyLoc.FunctionName(). If "SOMETHING" cannot be translated, it is output as "{{SOMETHING}}".
    /// Strings starting with '@' are not tranlated, and printed out as-is.
    /// </summary>
    public class Localizer : ALocalizer
    {
        #region constructors
        /// <summary>
        /// create a new localization engine, without an output stream or dictionary set.
        /// </summary>
        public Localizer() : base() { }
        /// <summary>
        /// create a new localization engine, without a dictionary of translations.
        /// </summary>
        /// <param name="output">Output goes to this if destination is unspecified.</param>
        public Localizer(TextWriter output) : base(output) { }
        /// <summary>
        /// create a new localization engine, without a default output stream.
        /// </summary>
        /// <param name="localizations">The source file to contain string localization definitions.</param>
        public Localizer(params string[] localizations) : base(localizations) { }
        /// <summary>
        /// create a new localization engine, without a default output stream.
        /// </summary>
        /// <param name="output">Output goes to this if destination is unspecified.</param>
        /// <param name="localizations">The source file to contain string localization definitions.</param>
        public Localizer(TextWriter output, params string[] localizations) : base(output, localizations) { }
        #endregion

        #region load/save functions
        /// <summary>
        /// Read the configuration from a set of file. Each file MUST have one "Localization_ID=" key, stating
        /// the identifier of the localization (such as "Localization_ID=en_US" or "Localization_ID=ru"), and one
        /// "Localization_Name=" key, stating the friendly name for the localization (such as "English, US" or
        /// "русский (Russian)"). If a default translation isn't set, we try to translate it.
        /// </summary>
        /// <param name="files">The files to load</param>
        /// <returns>This is the errors reported (internationalized, see "Localization Keys.txt"</returns>
        override public string[][] Load(params string[] files)
        {
            StringBuilder sb = new StringBuilder();
            List<string[]> errstrs = new List<string[]>();
            List<string> terr = new List<string>();
            foreach(string s in files)
            {
                try
                {
                    Dictionary<string, string> tmp = new Dictionary<string, string>();
                    ConfigFile.Read(s, ref tmp);
                    string Name;
                    string ID;

                    if(!tmp.TryGetValue("Localization_Name", out Name))
                        terr.Add("NO LOCALIZATION NAME");
                    tmp.TryGetValue("Localization_ID", out ID);
                    terr.Add("NO LOCALIZATION ID");
                    if(errstrs.Count > 0)
                    {
                        terr.Insert(0, "LOCALIZATION FILE ERROR E");
                        terr.Insert(0, "'");
                        terr.Insert(0, "@" + s);
                        terr.Insert(0, "'");
                        terr.Insert(0, "LOCALIZATION FILE ERROR S");

                        errstrs.Add(terr.ToArray());
                        terr.Clear();
                        continue;
                    }
                    Localization Loc;
                    if(!this.m_localizations.TryGetValue(ID, out Loc))
                    {
                        //might be the first load, which is set to the default translator
                        if(this.m_default_localization == null) this.m_default_localization = ID;
                        Loc = new Localization(tmp);
                        Loc.Name = Name;
                        Loc.ID = ID;
                        if(!Loc.ContainsKey("\'")) Loc["\n"] = "\'";
                        if(!Loc.ContainsKey("\"")) Loc["\n"] = "\"";
                        if(!Loc.ContainsKey(":")) Loc["\n"] = ":";
                        if(!Loc.ContainsKey(";")) Loc["\n"] = ";";

                        if(!Loc.ContainsKey("!")) Loc["\n"] = "!";
                        if(!Loc.ContainsKey("@")) Loc["\n"] = "@";
                        if(!Loc.ContainsKey("#")) Loc["\n"] = "#";
                        if(!Loc.ContainsKey("$")) Loc["\n"] = "$";
                        if(!Loc.ContainsKey("%")) Loc["\n"] = "%";
                        if(!Loc.ContainsKey("^")) Loc["\n"] = "^";
                        if(!Loc.ContainsKey("&")) Loc["\n"] = "&";
                        if(!Loc.ContainsKey("*")) Loc["\n"] = "*";
                        if(!Loc.ContainsKey("(")) Loc["\n"] = "(";
                        if(!Loc.ContainsKey(")")) Loc["\n"] = ")";
                        if(!Loc.ContainsKey(" ")) Loc["\n"] = " ";

                        //try loading translators
                        foreach(string k in Loc.Keys)
                        {
                            if(k.Substring(0, 11).Equals("!translator"))
                            {
                                string[] r = LoadTypeLocalizer(k, Loc[k]);
                                if(r != null)
                                    errstrs.Add(r);
                            }
                        }
                    }
                    else
                    {
                        if(Loc.Name != Name || Loc.ID != ID)
                        {
                            terr.Add("LOCALIZATION NAME ID MISMATCH S");
                            terr.Add("'");
                            terr.Add("@" + s);
                            terr.Add("'");
                            terr.Add("LOCALIZATION NAME ID MISMATCH E");
                            errstrs.Add(terr.ToArray());
                            terr.Clear();
                        }
                        Dictionary<string, string>.Enumerator en = tmp.GetEnumerator();
                        while(en.MoveNext())
                        {
                            Loc[en.Current.Key] = en.Current.Value;
                            if(en.Current.Key.Substring(0, 11).Equals("!translator"))
                            {
                                string[] r = LoadTypeLocalizer(en.Current.Key, en.Current.Value);
                                if(r != null)
                                    errstrs.Add(r);
                            }
                        }
                    }
                }
                catch(Exception e)
                {
                    terr.Add("FILE LOAD EXCEPTION");
                    terr.Add("@" + s);
                    terr.Add(":");
                    terr.Add(" ");
                    terr.Add("@" + e.Message);
                    errstrs.Add(terr.ToArray());
                    terr.Clear();
                }
            }
            return errstrs.ToArray();
        }
        /// <summary>
        /// Save the current translations (but not loaded functions/classes)
        /// </summary>
        /// <param name="file">The file to store the cinfiguration</param>
        /// <param name="localization">The localization to save</param>
        override public void Save(string file, string localization)
        {
            if(this.m_default_localization == null)
            {
                throw new NullReferenceException("Cannot localize, no default localization set.");
            }
            Localization l;
            if(!this.m_localizations.TryGetValue(localization, out l))
            {
                throw new ArgumentException(Translate_("LOCALIZATION DOES NOT EXIST S") + Translate_("'") + localization + Translate_("'") + Translate_("LOCALIZATION DOES NOT EXIST E"));
            }
            ConfigFile.Write(file, this.m_localizations[localization], null, null);
        }

        /// <summary>
        /// Loads a type localizer into the active translation set.
        /// </summary>
        /// <param name="key">They key this is assigned to</param>
        /// <param name="val">The assembly to load, in "assembly_file:class_name" format</param>
        /// <returns>Error information, null on success</returns>
        override public string[] LoadTypeLocalizer(string key, string val)
        {
            if(this.m_default_localization != null)
            { return new string[] { "No default localization set" }; }
            string[] toload = val.Split(':');
            if(toload.Length != 2)
            { return new string[] { "UNABLE TO DETERMINE ASSEMBLY S", "'", val, "'",
                                    "DEFINED BY",  "'", key, "'",
                                    "UNABLE TO DETERMINE ASSEMBLY E"}; }
            Assembly a = null;
            try
            { a = Assembly.Load(toload[0]); }
            catch(Exception)
            {
                try
                { a = Assembly.Load(Environment.CurrentDirectory + "/" + toload[0]); }
                finally {}
            }
            if(a == null)
            { return new string[] { "UANBLE TO LOAD ASSEMBLY S", "'", toload[0], "'", "UANBLE TO LOAD ASSEMBLY E"}; }

            Type t = null;

            try
            { t = a.GetType(toload[1]); }
            finally { }

            if(t == null)
            { return new string[] { "UNABLE TO FIND CLASS S", "'",
                toload[1], "'", "DEFINED BY", "'", toload[0], "'", "UNABLE TO FIND CLASS E" }; }

            PropertyInfo pi = t.GetProperty("Localizers");
            if(pi != null && pi.DeclaringType == typeof(TypeLocalizers))
            {
                TypeLocalizers localizer = (TypeLocalizers)pi.GetValue(null, null);
                foreach(Type tk in localizer.Keys)
                { this.m_type_localizers[this.m_default_localization][tk] = localizer[tk]; }
            }
            else
            { return new string[] { "UNABLE TO LOAD CLASS S", "'", toload[1], "'", "DEFINED BY", "'", toload[0], "'",
                                    "UNABLE TO LOAD CLASS E"}; }

            if(key.Length <= 11 || key.Substring(0, 11).Equals("!translator"))
            {
                key = "!translator" + key;
            }
            this.m_localizations[this.m_default_localization][key] = val;
            return null;
        }
        #endregion

        #region Core Translation Functions
        override protected internal bool requires_translate(ref string s)
        {
            if(s.Length > m_dont_translate.Length && s.IndexOf(m_dont_translate) == 0)
            {
                s = s.Substring(m_dont_translate.Length);
                return true;
            }
            return false;
        }
        /// <summary>
        /// Get a localization of an internationalized string
        /// </summary>
        /// <param name="val">The string to localize</param>
        /// <returns>The localized string, or the original string surrounded by "{{" and "}}" if no localization is available.</returns>
        override public string Translate(string val)
        {
            if(this.m_default_localization == null)
            {
                throw new NullReferenceException("Cannot localize, no default localization set.");
            }
            if(!requires_translate(ref val))
            {
                return val;
            }
            string trans;
            if(!this.m_localizations[this.m_default_localization].TryGetValue(val, out trans))
                trans = "{{" + val + "}}";
            return trans;
        }
        /// <summary>
        /// As Translate, but without checking for the existence of a default localization.
        /// </summary>
        override protected internal string Translate_(string val)
        {
            if(!requires_translate(ref val))
            {
                return val;
            }
            string trans;
            if(!this.m_localizations[this.m_default_localization].TryGetValue(val, out trans))
                trans = "{{" + val + "}}";
            return trans;
        }
        /// <summary>
        /// Get a localization of an internationalized string
        /// </summary>
        /// <param name="lang">The language to localize to</param>
        /// <param name="val">The string to localize</param>
        /// <returns>The localized string, or the original string surrounded by "{{" and "}}" if no localization is available.</returns>
        override public string Translate(string lang, string val)
        {
            if(!this.m_localizations.ContainsKey(lang))
            {
                throw new NullReferenceException("Cannot localize, localization '" + lang + "' does not exist.");
            }
            if(!requires_translate(ref val))
            {
                return val;
            }
            string trans;
            if(!this.m_localizations[lang].TryGetValue(val, out trans))
                trans = "{{" + val + "}}";
            return trans;
        }
        /// <summary>
        /// Get a localization of an internationalized string
        /// </summary>
        /// <param name="lang">The language to localize to</param>
        /// <param name="val">The string to localize</param>
        /// <returns>The localized string, or the original string surrounded by "{{" and "}}" if no localization is available.</returns>
        override protected internal string Translate_(string lang, string val)
        {
            if(!requires_translate(ref val))
            {
                return val;
            }
            string trans;
            if(!this.m_localizations[lang].TryGetValue(val, out trans))
                trans = "{{" + val + "}}";
            return trans;
        }
        /// <summary>
        /// Get a localization of a non string, defaulting to ToString
        /// </summary>
        /// <param name="val">The value to localize</param>
        /// <returns>The localized string, converted by the default translat or inone is available.</returns>
        override public string Translate(object val)
        {
            if(this.m_default_localization == null)
            {
                throw new NullReferenceException("Cannot localize, no default localization set.");
            }
            LocalizeFunction lf;
            if(!m_type_localizers[this.m_default_localization].TryGetValue(val.GetType(), out lf))
            { lf = c_default_translator; }
            return lf(val);
        }
        /// <summary>
        /// Get a localization of a non string, defaulting to ToString
        /// </summary>
        /// <param name="val">The value to localize</param>
        /// <returns>The localized string, converted by the default translat or inone is available.</returns>
        override protected internal string Translate_(object val)
        {
            LocalizeFunction lf;
            if(!m_type_localizers[this.m_default_localization].TryGetValue(val.GetType(), out lf))
            { lf = c_default_translator; }
            return lf(val);
        }
        /// <summary>
        /// Get a localization of an internationalized string
        /// </summary>
        /// <param name="lang">The language to localize to</param>
        /// <param name="val">The string to localize</param>
        /// <returns>The localized string, or the original string surrounded by "{{" and "}}" if no localization is available.</returns>
        override public string Translate(string lang, object val)
        {
            if(!this.m_localizations.ContainsKey(lang))
            {
                throw new NullReferenceException("Cannot localize, localization '" + lang + "' does not exist.");
            }
            LocalizeFunction lf;
            if(!m_type_localizers[lang].TryGetValue(val.GetType(), out lf))
            { lf = c_default_translator; }
            return lf(val);
        }
        /// <summary>
        /// Get a localization of an internationalized string
        /// </summary>
        /// <param name="lang">The language to localize to</param>
        /// <param name="val">The string to localize</param>
        /// <returns>The localized string, or the original string surrounded by "{{" and "}}" if no localization is available.</returns>
        override protected internal string Translate_(string lang, object val)
        {
            LocalizeFunction lf;
            if(!m_type_localizers[lang].TryGetValue(val.GetType(), out lf))
            { lf = c_default_translator; }
            return lf(val);
        }
        #endregion

        #region Generic Object TextWriter-like implementation
        protected static readonly Type[] m_stringargs = new Type[] { typeof(string) };
        /// <summary>
        /// Attempt to write to the target. This is done by looking for static functions or properites
        /// in the target's type. It writes to the first public matching member found. The targets
        /// checked (in order) are: Method 'Write(string)', Method 'Append(string)',
        /// Property 'string Text', Property 'string Content', Property 'object Content'.
        /// If the target is a property of type 'object', it will check if the returned value is a string,
        /// and if so, the text will be appended. If not, the value will be set to "", and then the new
        /// text appended. BE CAREFUL, this can cause an error
        /// </summary>
        /// <param name="o">The object to place the translated text into</param>
        /// <param name="s">The string of translated text</param>
        override public void WriteToGeneric(object o, string s)
        {
            Type t = o.GetType();
            MethodInfo meth = t.GetMethod("Write", m_stringargs);
            if(meth != null && meth.IsPublic)
            {
                meth.Invoke(o, new string[] { s });
                return;
            }
            meth = t.GetMethod("Append", m_stringargs);
            if(meth != null && meth.IsPublic)
            {
                meth.Invoke(o, new string[] { s });
                return;
            }
            PropertyInfo prop = t.GetProperty("Text", typeof(string));
            if(prop != null && prop.CanWrite)
            {
                string st = (string)prop.GetValue(o, null);
                if(st != null) s = st + s;
                prop.SetValue(o, s, null);
            }
            prop = t.GetProperty("Text");
            if(prop != null && prop.CanWrite)
            {
                if(prop.PropertyType == typeof(string))
                {
                    string st = (string)prop.GetValue(o, null);
                    if(st != null) s = st + s;
                    prop.SetValue(o, s, null);
                }
                else if(prop.PropertyType == typeof(object))
                {
                    object ob = prop.GetValue(o, null);
                    if(ob.GetType() == typeof(string) && ob != null) s = (string)ob + s;
                    prop.SetValue(o, s, null);
                }
                return;
            }

        }
        #endregion
    }
}
